<?php
/**
 *
 * Mysql based cache, for shared directory between clustered servers
 * @author Fernando Palomo Garcia fernando@kernelius.com
 * @version 0.1
 * @license Apache License 2.0
 */
class Kernelius_Cache_Persistent_Mysql extends Kernelius_Cache_Storage
    implements Kernelius_Cache_Persistent{

    /**
     * prefix for cache tables in db
     */
    const TABLE_PREFIX="cache_";

    /**
     * table name after prefix size. the bigger this is, the more tables there will be,
     * and the lower number or rows in each table
     */
    const TABLE_NAME_SIZE="5";

    /**
     * default mysql port
     */
    const DEFAULT_PORT=3306;

    /**
     * default mysql host
     */
    const DEFAULT_HOST="localhost";

    /**
     * key field in table
     */
    const KEY_FIELD="key_name";

    /**
     * value field in table
     */
    const VALUE_FIELD="key_value";

    /**
     * expiration date in table
     */
    const EXPIRES_FIELD="expires";

    /**
     * mysql binary script
     */
    const MYSQL_BIN="/usr/bin/mysql";

    /**
     * db user
     * @var string
     */
    protected $user;

    /**
     * db password
     * @var string
     */
    protected $password;

    /**
     * data base
     * @var string
     */
    protected $db;

    /**
     * mysql host 
     * @var string
     */
    protected $host;

    /**
     * mysql port
     * @var int
     */
    protected $port;

    /**
     * mysqli object to access db
     * @var mysqli
     */
    protected $_mysqli;


    /**
     * constructor receives config array
     * @param array $config
     */
    public function __construct($config){
        $this->user= isset($config["user"])?$config["user"]: Kernelius_Logger::exception ("user undefined");
        $this->password= isset($config["password"])?$config["password"]: Kernelius_Logger::exception ("password undefined");
        $this->db= isset($config["db"])?$config["db"]: Kernelius_Logger::exception(" db name undefined");
        $this->host= isset($config["host"])?$config["host"]:self::DEFAULT_HOST;
        $this->port =isset($config["port"])?$config["port"]:self::DEFAULT_PORT;
        $this->_mysqli= new mysqli($this->host, $this->user, $this->password, $this->db, $this->port);
        if (mysqli_connect_errno()) {
            Kernelius_Logger::exception("ERROR ".mysql_connect_errno());
            throw new exception("Connect failed: %s\n", mysqli_connect_error());
        }

    }


    /**
     * create route for this key
     * @param string $key
     */
    protected function createTable($key){
        $table = $this->getTableName($key);
        $sql_create=
        "CREATE TABLE $table ( ".
        "key_name VARCHAR( 32 ) NOT NULL ,".
        "key_value BLOB NOT NULL ,".
        "expires INT UNSIGNED NOT NULL ,".
        "PRIMARY KEY ( key_name )".
        ") ENGINE = MYISAM ";
        $sql_search="show tables like '$table'";

        $res= $this->_mysqli->query($sql_search);
        if(!$res->num_rows){
            Kernelius_Logger::log("create table $table");
            $this->_mysqli->query($sql_create);
        }
        return $table;
    }


    /**
     * get path for this wrapped key
     * @param string $key wrapped key
     * @return string 
     */
    protected function getTableName($key){
        return self::TABLE_PREFIX.substr($key,0,self::TABLE_NAME_SIZE);
    }


    /**
     * set value
     * //TODO check if key was saved correctly
     * @param  $key
     * @param  $value
     * @param  $timeout
     * @return true
     */
    public function set($key, $value, $timeout=Kernelius_Cache::DEFAULT_EXPIRY){
        $wrapped_key=$this->wrap($key);
        $table= $this->getTableName($wrapped_key);
        Kernelius_Logger::log(" set $key to db ");
        $this->createTable($wrapped_key);
        $sql="INSERT DELAYED into $table (".
                self::KEY_FIELD . ",". self::VALUE_FIELD. "," .self::EXPIRES_FIELD.
                ") values ('".
                $wrapped_key."', '".mysql_escape_string($value). "','".( $timeout+time())."')".
                " ON DUPLICATE KEY UPDATE ".self::VALUE_FIELD."='".mysql_escape_string($value)."',".
                self::EXPIRES_FIELD."='".(time()+$timeout)."'";
        return $this->executeInBackground( $sql);
    }


    /**
     *  get key
     * @param string $key
     * @return object
     */
    public function get($key){
        $wrapped_key= $this->wrap($key);
        $table= $this->getTableName($wrapped_key);
        Kernelius_Logger::log(" get $key from db ");
        $sql= "SELECT value from $table where ". 
        self::KEY_FIELD. "='". $wrapped_key. " and ".
        self::EXPIRES_FIELD. " >= ". time()." LIMIT 1";
        
        if($res= $this->_mysqli->query($sql)===FALSE){
            return null;
        }
        var_dump($res);
        if($res->num_rows){
            $row= $res->fetch_array(self::VALUE_FIELD);
            return $row[$field_name];
        }
        return null;
    }


    /**
     * delete key
     * @param string $key
     */
    public function delete($key){
        $wrapped_key=$this->wrap($key);
        $table= $this->getTableName($wrapped_key);
        Kernelius_Logger::log(" delete $key from db ");
        $sql=" DELETE LOW PRIORITY FROM $table where ".self::KEY_FIELD."='$wrapped_key' LIMIT 1;";
        //$this->_mysqli->query($sql);
        $this->executeInBackground($sql);
        return true;
    }


    /**
     * touch file , refresh mtime
     * @param string $key
     */
    public function touch($key){
        $wrapped_key=$this->wrap($key);
        $table= $this->getTableName($wrapped_key);
        Kernelius_Logger::log(" touch $key in db ");
        $sql="UPDATE LOW PRIORITY $table set ".self::EXPIRES_FIELD."= ". (time()+Kernelius_Cache::DEFAULT_EXPIRY). " LIMIT 1";
        $this->executeInBackground($sql);
        return true;
    }


    /**
     * execute query in background. launch process and don't wait for it
     * @param string $sql
     */
    protected function executeInBackground($sql){
        Kernelius_Logger::Log("Mysql background: $sql");
        $cmd= self::MYSQL_BIN." -u ".$this->user.
                    " -p".$this->password.
                    " -h ".$this->host.
                    " -P ".$this->port.
                    " -D ".$this->db.
                    " -e \" $sql \" & ";
        Kernelius_Logger::Log("command: $cmd");
        pclose(popen("$cmd ","r"));
    }

    /**
     * delete files older than $older minutes
     * @param int $older
     */
    public function gc($older){
        foreach($this->getAllTables() as $table){
            $sql_1="DELETE LOW PRIORITY FROM $table WHERE expires < ".(time());
            $sql_2="OPTIMIZE $table";
            $this->executeInBackground($sql_1);
            $this->executeInBackground($sql_2);
        }
    }


    /**
     * get all cache tables
     * @return array
     */
    protected function getAllTables(){
        $ret=array();
        $sql="Show tables like '".self::TABLE_PREFIX."%'";
        $res= $this->_mysqli->query($sql);
        while($row= $res->fetch_array()){
            $ret[]=$row[0];
        }
        return $ret;
    }

}